package com.chen.time.timing;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;

import javax.swing.Timer;

import com.chen.time.timing.interpolation.Interpolator;
import com.chen.time.timing.interpolation.LinearInterpolator;

public final class Animator {

    private TimingSource timer;
    private TimingSource swingTimer;
    private Handler handler;
    private List<TimingListener> listeners = new ArrayList<TimingListener>();

    private long startTime;
    private long currentStartTime;
    private boolean intRepeatCount = true;
    private boolean timeToStop;
    private boolean hasBegun;
    private long pauseBeginTime;
    private boolean running;

    private double repeatCount = 1.0;
    private int startDelay;
    private RepeatBehavior repeatBehavior = RepeatBehavior.REVERSE;
    private EndBehavior endBehavior = EndBehavior.HOLD;

    private int duration;
    private int resolution = 20;
    private float acceleration;
    private float deceleration;
    private float startFraction;
    private Direction direction = Direction.FORWARD;
    private Interpolator interpolator = LinearInterpolator.getInstance();

    public static final int INFINITE = -1;

    public static enum EndBehavior {
        HOLD, RESET,
    };

    public static enum Direction {
        FORWARD, BACKWARD,
    };

    public static enum RepeatBehavior {
        LOOP, REVERSE
    };

    public Animator(int duration) {
        this(duration, null);
    }

    public Animator(int duration, TimingListener listener) {
        this.duration = duration;
        addTimingListener(listener);
        // java.awt.Toolkit.getDefaultToolkit();
        swingTimer = new Handler();
        timer = swingTimer;
    }

    public Animator(int duration, double repeatCount, RepeatBehavior repeatBehavior, TimingListener listener) {
        this(duration, listener);
        validateRepeatCount(repeatCount);
        this.repeatCount = repeatCount;
        this.repeatBehavior = repeatBehavior != null ? repeatBehavior : RepeatBehavior.REVERSE;
        intRepeatCount = Math.rint(repeatCount) == repeatCount;
    }

    private void validateRepeatCount(double repeatCount) {
        if (repeatCount < 1 && repeatCount != INFINITE) {
            throw new IllegalArgumentException("repeatCount (" + repeatCount + ") cannot be <= 0");
        }
    }

    public void setStartDirection(Direction startDirection) {
        throwExceptionIfRunning();
        this.direction = startDirection;
    }

    public void setInterpolator(Interpolator interpolator) {
        throwExceptionIfRunning();
        this.interpolator = interpolator;
    }

    public void setAcceleration(float acceleration) {
        throwExceptionIfRunning();
        if (acceleration < 0 || acceleration > 1) {
            throw new IllegalArgumentException("Acceleration value cannot lie" + " outside [0,1] range");
        }
        if (acceleration > 1 - deceleration) {
            throw new IllegalArgumentException("Acceleration value cannot be"
                    + " greater than (1 - deceleration)");
        }
        this.acceleration = acceleration;
    }

    public void setDeceleration(float deceleration) {
        throwExceptionIfRunning();
        if (deceleration < 0 || deceleration > 1) {
            throw new IllegalArgumentException("Deceleration value cannot lie" + " outside [0,1] range");
        }
        if (deceleration > 1 - acceleration) {
            throw new IllegalArgumentException("Deceleration value cannot be"
                    + " greater than (1 - acceleration)");
        }
        this.deceleration = deceleration;
    }

    public void addTimingListener(TimingListener listener) {
        if (listener != null) {
            synchronized (listeners) {
                if (!listeners.contains(listener)) {
                    listeners.add(listener);
                }
            }
        }
    }

    public void removeTarget(TimingListener listener) {
        synchronized (listeners) {
            listeners.remove(listener);
        }
    }

    private void throwExceptionIfRunning() {
        if (running) {
            throw new IllegalStateException("Cannot perform this operation " + "while Animator is running");
        }
    }

    public void setResolution(int resolution) {
        if (resolution < 0) {
            throw new IllegalArgumentException("resolution must be >= 0");
        }
        throwExceptionIfRunning();
        this.resolution = resolution;
        timer.setResolution(resolution);
    }

    public void setDuration(int duration) {
        throwExceptionIfRunning();
        this.duration = duration;
    }

    public void setRepeatCount(double repeatCount) {
        validateRepeatCount(repeatCount);
        throwExceptionIfRunning();
        this.repeatCount = repeatCount;
    }

    public void setStartDelay(int startDelay) {
        if (startDelay < 0) {
            throw new IllegalArgumentException("startDelay (" + startDelay + ") cannot be < 0");
        }
        throwExceptionIfRunning();
        this.startDelay = startDelay;
        timer.setStartDelay(startDelay);
    }

    public void setRepeatBehavior(RepeatBehavior repeatBehavior) {
        throwExceptionIfRunning();
        this.repeatBehavior = repeatBehavior != null ? repeatBehavior : RepeatBehavior.REVERSE;
    }

    public void setEndBehavior(EndBehavior endBehavior) {
        throwExceptionIfRunning();
        this.endBehavior = endBehavior;
    }

    public float getStartFraction() {
        return startFraction;
    }

    public void setStartFraction(float startFraction) {
        if (startFraction < 0 || startFraction > 1) {
            throw new IllegalArgumentException("initialFraction must be " + "between 0 and 1");
        }
        throwExceptionIfRunning();
        this.startFraction = startFraction;
    }

    public void start() {
        throwExceptionIfRunning();
        hasBegun = false;
        running = true;
        // Initialize start time variables to current time
        startTime = System.nanoTime() / 1000000 + startDelay;
        if (duration != INFINITE
                && (direction == Direction.FORWARD && startFraction > 0 || direction == Direction.BACKWARD
                        && startFraction < 1)) {
            float offsetFraction = direction == Direction.FORWARD ? startFraction : 1 - startFraction;
            long startDelta = (long) (duration * offsetFraction);
            startTime -= startDelta;
        }
        currentStartTime = startTime;
        timer.start();
    }

    public boolean isRunning() {
        return running;
    }

    public void stop() {
        timer.stop();
        end();
        timeToStop = false;
        running = false;
        pauseBeginTime = 0;
    }

    public void cancel() {
        timer.stop();
        timeToStop = false;
        running = false;
        pauseBeginTime = 0;
    }

    public void pause() {
        if (isRunning()) {
            pauseBeginTime = System.nanoTime();
            running = false;
            timer.stop();
        }
    }

    public void resume() {
        if (pauseBeginTime > 0) {
            long pauseDelta = (System.nanoTime() - pauseBeginTime) / 1000000;
            startTime += pauseDelta;
            currentStartTime += pauseDelta;
            timer.start();
            pauseBeginTime = 0;
            running = true;
        }
    }

    private void doTimingEvent(float fraction) {
        synchronized (listeners) {
            for (int i = 0; i < listeners.size(); ++i) {
                listeners.get(i).timingEvent(fraction);
            }
        }
        if (timeToStop) {
            stop();
        }
    }

    private void begin() {
        synchronized (listeners) {
            for (int i = 0; i < listeners.size(); ++i) {
                listeners.get(i).begin();
            }
        }
    }

    private void end() {
        synchronized (listeners) {
            for (int i = 0; i < listeners.size(); ++i) {
                listeners.get(i).end();
            }
        }
    }

    private void repeat() {
        synchronized (listeners) {
            for (int i = 0; i < listeners.size(); ++i) {
                listeners.get(i).repeat();
            }
        }
    }

    private float timingEventPreprocessor(float fraction) {
        if (acceleration != 0 || deceleration != 0) {
            float runRate = 1 / (1 - acceleration / 2 - deceleration / 2);
            if (fraction < acceleration) {
                float averageRunRate = runRate * fraction / acceleration / 2;
                fraction *= averageRunRate;
            } else if (fraction > 1 - deceleration) {
                float tdec = fraction - (1 - deceleration);
                float pdec = tdec / deceleration;
                fraction = runRate * (1 - acceleration / 2 - deceleration + tdec * (2 - pdec) / 2);
            } else {
                fraction = runRate * (fraction - acceleration / 2);
            }

            if (fraction < 0) {
                fraction = 0;
            } else if (fraction > 1) {
                fraction = 1;
            }
        }

        return interpolator.interpolate(fraction);
    }

    public long getTotalElapsedTime(long currentTime) {
        return currentTime - startTime;
    }

    public long getTotalElapsedTime() {
        long currentTime = System.nanoTime() / 1000000;
        return getTotalElapsedTime(currentTime);
    }

    public long getCycleElapsedTime(long currentTime) {
        return currentTime - currentStartTime;
    }

    public long getCycleElapsedTime() {
        long currentTime = System.nanoTime() / 1000000;
        return getCycleElapsedTime(currentTime);
    }

    public float getTimingFraction() {
        long currentTime = System.nanoTime() / 1000000;
        long cycleElapsedTime = getCycleElapsedTime(currentTime);
        long totalElapsedTime = getTotalElapsedTime(currentTime);
        double currentCycle = (double) totalElapsedTime / duration;
        float fraction;

        if (!hasBegun) {
            // Call begin() first time after calling start()
            begin();
            hasBegun = true;
        }
        if (duration != INFINITE && repeatCount != INFINITE && currentCycle >= repeatCount) {
            // Envelope done: stop based on end behavior
            switch (endBehavior) {
            case HOLD:
                // Make sure we send a final end value
                if (intRepeatCount) {
                    // If supposed to run integer number of cycles, hold
                    // on integer boundary
                    if (direction == Direction.BACKWARD) {
                        // If we were traveling backward, hold on 0
                        fraction = 0f;
                    } else {
                        fraction = 1f;
                    }
                } else {
                    // hold on final value instead
                    fraction = Math.min(1, (float) cycleElapsedTime / duration);
                }
                break;
            case RESET:
                // RESET requires setting the final value to the start value
                fraction = 0;
                break;
            default:
                fraction = 0;
                // should not reach here
                break;
            }
            timeToStop = true;
        } else if (duration != INFINITE && cycleElapsedTime > duration) {
            // Cycle end: Time to stop or change the behavior of the timer
            long actualCycleTime = cycleElapsedTime % duration;
            fraction = (float) actualCycleTime / duration;
            // Set new start time for this cycle
            currentStartTime = currentTime - actualCycleTime;

            if (repeatBehavior == RepeatBehavior.REVERSE) {
                boolean oddCycles = (int) (cycleElapsedTime / duration) % 2 > 0;
                if (oddCycles) {
                    // reverse the direction
                    direction = direction == Direction.FORWARD ? Direction.BACKWARD : Direction.FORWARD;
                }
                if (direction == Direction.BACKWARD) {
                    fraction = 1 - fraction;
                }
            }
            repeat();
        } else {
            // mid-stream: calculate fraction of animation between
            // start and end times and send fraction to listener
            fraction = 0;
            if (duration != INFINITE) {
                // Only limited duration animations need a fraction
                fraction = (float) cycleElapsedTime / duration;
                if (direction == Direction.BACKWARD) {
                    fraction = 1 - fraction;
                }
                fraction = Math.min(fraction, 1);
                fraction = Math.max(fraction, 0);
            }
        }
        return timingEventPreprocessor(fraction);
    }

    public synchronized void setTimer(TimingSource timer) {
        throwExceptionIfRunning();
        if (this.timer != swingTimer) {
            this.timer.removeEventListener(handler);
        }
        if (timer == null) {
            this.timer = swingTimer;
        } else {
            this.timer = timer;
            if (handler == null) {
                handler = new Handler();
            }
            timer.addEventListener(handler);
        }
        this.timer.setResolution(resolution);
        this.timer.setStartDelay(startDelay);
    }

    class Handler extends TimingSource implements TimingEventListener, ActionListener {

        private Timer theTimer = new Timer(resolution, this);

        @Override
        public void start() {
            theTimer.start();
        }

        @Override
        public void stop() {
            theTimer.stop();
        }

        @Override
        public void setResolution(int resolution) {
            theTimer.setDelay(resolution);
        }

        @Override
        public void setStartDelay(int delay) {
            theTimer.setInitialDelay(delay);
        }

        @Override
        public void timingSourceEvent(TimingSource timingSource) {
            if (timer == timingSource && running) {
                doTimingEvent(getTimingFraction());
            }
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            doTimingEvent(getTimingFraction());
        }
    }
}
